5  NumPy 数组的创建与访问

5.1 引言NumPy在金融计算中的核心地位

NumPy(Numerical Python)是Python科学计算的基础包,提供了高性能的多维数组对象和相关工具。在金融数据分析中,NumPy的地位无可替代,它不仅是Pandas、Matplotlib等库的基础,其本身也提供了强大的数值计算能力。

理论背景:数组与列表的本质区别

从数据结构的角度来看,NumPy数组与Python列表有根本性差异:

特性 Python列表 NumPy数组
内存存储 分散存储对象引用 连续内存存储
元素类型 可以不同 必须相同
性能 解释执行,较慢 编译优化,极快
向量化 不支持 原生支持
内存效率 较低(每个元素8字节+对象开销) 较高(紧凑存储)

为什么NumPy更快? 1. 连续内存: 缓存友好,减少内存访问延迟 2. 类型一致: 避免动态类型检查 3. 向量化操作: 底层C/Fortran实现,利用SIMD指令 4. 广播机制: 自动对齐不同形状的数组

5.2 数组的创建

5.2.1 基础创建方法

NumPy提供了多种创建数组的方法:

列表 5.1
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import numpy as np  # 导入NumPy数值计算库
# 已知股价数据
stock1=np.array([22.91,22.89,23.38,23.09,22.90])
stock2=np.array([41.14,40.99,41.29,40.81,40.97])  # 创建NumPy数组stock2
# 1. 查看股票1的属性值情况,并输出。
print(stock1.shape)
# 2. 输出股票2周二的股价
print(stock2[1])
# 3. 连接股票1与股票2的数据(按照2X5的方式连接),并输出
stock_con=np.concatenate(([stock1],[stock2]),axis=0)
print(stock_con)  # 输出两只股票收盘价合并后的2×5矩阵

代码深度解析:

  1. np.array()的参数:

    np.array(object, dtype=None, copy=True, order=None, ndmin=0)
    • object: 列表、元组或其他数组
    • dtype: 指定数据类型(如np.float64, np.int32)
    • copy: 是否复制数据
  2. 数据类型的金融意义:

    • float32: 节省内存,适合大规模数据
    • float64: 双精度,金融计算推荐
    • int64: 整数(如成交量、日期)
  3. 内存布局:

    • C顺序(C-style): 行优先(默认)
    • F顺序(Fortran-style): 列优先

5.2.2 二维数组的创建

列表 5.2
# ==================== 创建二维数组 ====================
# np.array()可以从嵌套列表创建二维数组
# 外层列表包含2个元素(2行)
# 每个元素是一个包含5个数值的列表(5列)
# 第一行存储股票1的5日收盘价
# 第二行存储股票2的5日收盘价
stocks_2d = np.array([  # 创建二维数组(2行5列)
    [22.91, 22.89, 23.38, 23.09, 22.90],  # 第1行:股票1的收盘价
    [41.14, 40.99, 41.29, 40.81, 40.97]   # 第2行:股票2的收盘价
])

# ==================== 查看数组形状 ====================
# .shape属性返回数组的维度
# 对于二维数组,返回(行数, 列数)
print("二维数组形状:", stocks_2d.shape)  # 输出:(2, 5)

# .shape[0]访问第一个维度(行数)
print("行数:", stocks_2d.shape[0])  # 输出:2

# .shape[1]访问第二个维度(列数)
print("列数:", stocks_2d.shape[1])  # 输出:5

# ==================== 查看数组属性 ====================
# .ndim属性返回数组的维度数(秩)
# 2表示这是一个二维数组
print(f"\n数组维度: {stocks_2d.ndim}")  # 输出:2

# .size属性返回数组中元素的总数
# 2×5=10个元素
print(f"数组大小: {stocks_2d.size}")  # 输出:10

# .dtype属性返回数组的数据类型
# float64表示64位浮点数
print(f"数据类型: {stocks_2d.dtype}")  # 输出:float64

补充说明:shape属性的理解

shape属性返回一个元组,描述数组的维度: - 一维数组(5,): 5个元素 - 二维数组(2, 5): 2行5列 - 三维数组(2, 3, 4): 2个3×4的矩阵

在金融应用中: - 一维: 单只股票的时间序列 - 二维: 多只股票的时间序列 - 三维: 多个市场、多只股票、多日数据(面板数据)

5.3 数组的访问与切片

5.3.1 一维数组的索引

列表 5.3
# ==================== 任务2:输出股票2周二的股价 ====================
# 假设索引0=周一,1=周二,2=周三,3=周四,4=周五
# 方括号[]用于索引访问
# stock2[1]访问数组中索引为1的元素(第2个元素)
tuesday_price_stock2 = stock2[1]  # 获取索引1的元素(周二股价)
# f-string格式化输出,.2f保留2位小数
print(f"股票2周二股价: {tuesday_price_stock2:.2f}元")  # 输出:40.99元

# ==================== 多种索引方式 ====================
# 切片语法:[start:end:step]
# start是起始索引(包含)
# end是结束索引(不包含)
# step是步长
# stock2[0:3]获取索引0,1,2的元素(前3个)
print(f"\n股票2第1-3天: {stock2[0:3]}")  # 输出前3天价格

# 负数索引从末尾开始计数
# -1是最后一个元素,-2是倒数第2个
# stock2[-2:]从倒数第2个到末尾
print(f"股票2最后2天: {stock2[-2:]}")  # 输出后2天价格

# 步长为2,每隔一个取一个元素
# stock2[::2]取索引0,2,4的元素
print(f"股票2偶数天: {stock2[::2]}")  # 输出奇数日的价格(索引0,2,4)

# ==================== 布尔索引(高级用法) ====================
# stock2 > 41.0返回布尔数组
# 比较运算符>逐元素比较
# 结果是[False, False, True, False, False]
# 使用布尔数组作为索引,返回True对应的元素
high_price_days = stock2[stock2 > 41.0]  # 筛选价格大于41的元素
print(f"\n股价>41元的交易日: {high_price_days}")  # 输出:[41.29]

索引规则总结 - [start:end:step] start包含,end不包含 - 负数索引从末尾计数 -1是最后一个元素 - 省略start默认从0开始,省略end到末尾 - ::step[:​:-1]用于反向

5.3.2 二维数组的索引

列表 5.4
# ==================== 获取单个元素 ====================
# 二维数组索引语法:array[row, col]
# row是行索引,col是列索引
# stocks_2d[0, 2]表示第1行(索引0),第3列(索引2)
element = stocks_2d[0, 2]  # 获取第1行第3列的元素
print(f"stocks_2d[0, 2] = {element}")  # 输出:23.38

# ==================== 获取整行 ====================
# :表示选择该维度的所有元素
# stocks_2d[1, :]表示第2行(索引1)的所有列
row_1 = stocks_2d[1, :]  # 获取第2行的所有元素
print(f"\n股票2所有交易日价格: {row_1}")  # 输出第2行数据

# ==================== 获取整列 ====================
# stocks_2d[:, 2]表示所有行的第3列(索引2)
# :表示选择所有行
col_3 = stocks_2d[:, 2]  # 获取第3列的所有元素
print(f"周三所有股票价格: {col_3}")  # 输出第3列数据

# ==================== 切片:获取子矩阵 ====================
# stocks_2d[:, 1:4]表示所有行,第2-4列
# 1:4表示列索引1,2,3(不包含4)
# 结果是一个2×3的子矩阵
sub_matrix = stocks_2d[:, 1:4]  # 获取所有行的第2-4列
print(f"\n周二到周四的价格:\n{sub_matrix}")  # 输出子矩阵

金融应用场景: - 单只股票时间序列: arr[stock_id, :] - 单日所有股票: arr[:, day_id] - 特定时间段: arr[:, start:end]

5.4 数组的拼接与分割

5.4.1 concatenate()数组拼接

列表 5.5
# ==================== 任务3:连接股票1与股票2 ====================
# np.concatenate()沿指定轴连接数组
# 参数1:([stock1], [stock2])是要连接的数组序列
# 注意:[stock1]和[stock2]必须包装在列表中
# axis=0指定沿第0轴(行方向)拼接
stock_con = np.concatenate(([stock1], [stock2]), axis=0)  # 垂直拼接两个一维数组

# ==================== 输出拼接结果 ====================
print("拼接后的数组:")  # 输出标题
print(stock_con)  # 输出拼接后的2×5数组
print(f"形状: {stock_con.shape}")  # 输出:(2, 5)

# ==================== 理解axis参数 ====================
# axis=0:沿行方向拼接(垂直堆叠)
# axis=1:沿列方向拼接(水平连接)

# ==================== 水平拼接示例 ====================
# 创建新的价格数据
# prices_aug包含2个新价格
prices_aug = np.array([22.95, 23.02])  # 待追加的价格数据

# np.concatenate()沿axis=0拼接
# 将stock2和prices_aug首尾相接
# 结果是包含7个元素的一维数组
stock2_aug = np.concatenate((stock2, prices_aug), axis=0)  # 拼接数组
print(f"\n扩充后的股票2: {stock2_aug}")  # 输出拼接后的数组

# ==================== 二维数组的拼接 ====================
# np.vstack()垂直堆叠(沿行方向)
# 等价于np.concatenate(..., axis=0)
# 将stock1和stock2作为两行堆叠
stock_vstack = np.vstack((stock1, stock2))  # 垂直堆叠

# np.hstack()水平连接(沿列方向)
# 等价于np.concatenate(..., axis=1)
# 将stock1和stock2首尾相接成一行
stock_hstack = np.hstack((stock1, stock2))  # 水平连接

# ==================== 输出拼接结果 ====================
print(f"\n垂直堆叠:\n{stock_vstack}")  # 输出2×5数组
print(f"水平连接:\n{stock_hstack}")  # 输出包含10个元素的一维数组

拼接函数对比:

函数 功能 axis参数 等价操作
concatenate 通用拼接 指定轴 -
vstack 垂直堆叠 固定0 concatenate(..., axis=0)
hstack 水平连接 固定1 concatenate(..., axis=1)
stack 创建新维度 新增 -

5.4.2 数组的分割

列表 5.6
# ==================== 水平分割(按列) ====================
# np.hsplit()水平分割数组(按列切分)
# stocks_2d是要分割的二维数组
# [2, 4]指定分割位置(在索引2和4之后分割)
# 结果是3个数组:列0-1,列2-3,列4
stocks_split = np.hsplit(stocks_2d, [2, 4])  # 在第2和第4列后分割

# ==================== 输出分割结果 ====================
print("按列分割结果:")  # 输出标题
# enumerate()遍历分割后的数组列表
# i是索引,part是分割后的数组部分
for i, part in enumerate(stocks_split):  # 遍历每个部分
    # .shape获取数组形状
    print(f"  部分{i+1}: {part.shape}")  # 输出每部分的形状

# ==================== 垂直分割(按行) ====================
# np.vsplit()垂直分割数组(按行切分)
# stocks_2d是要分割的二维数组
# 参数2表示分成2行
# 结果是2个数组,每行一个
stocks_vsplit = np.vsplit(stocks_2d, 2)  # 分成2行

# ==================== 输出分割结果 ====================
print(f"\n按行分割:")  # 输出标题
# enumerate()遍历分割后的数组列表
for i, part in enumerate(stocks_vsplit):  # 遍历每一行
    print(f"  部分{i+1}: {part}")  # 输出每行的内容

金融应用: - 分割训练/测试集: 时间序列数据的分割 - 按市场分组: 沪深股票数据分离 - 按时间段: 日内/隔夜数据分离

5.5 数组的视图与副本

关键概念:理解视图(View)和副本(Copy)的区别对避免意外数据修改至关重要。

列表 5.7
# ==================== 创建原始数组 ====================
# np.array()创建一个包含5个元素的数组
original = np.array([1, 2, 3, 4, 5])  # 原始数组

# ==================== 切片返回视图 ====================
# 切片操作[1:4]返回视图(View)
# 视图共享原始数组的内存
# 修改视图会影响原数组
view = original[1:4]  # 创建切片视图
# 修改视图的第1个元素
view[0] = 999  # 修改视图的元素
# 观察到原数组也被修改
print(f"修改视图后,原数组: {original}")  # 输出:[1, 999, 3, 4, 5]

# ==================== 创建副本 ====================
# .copy()方法创建数组的副本(Copy)
# 副本拥有独立的内存
# 修改副本不影响原数组
copy = original[1:4].copy()  # 创建副本
# 修改副本的第1个元素
copy[0] = 888  # 修改副本的元素
# 原数组不受影响
print(f"修改副本后,原数组: {original}")  # 输出:[1, 999, 3, 4, 5]

# ==================== 重置原数组 ====================
# 重新赋值创建新数组
original = np.array([1, 2, 3, 4, 5])  # 重置为原始值

# ==================== Fancy Indexing ====================
# 使用列表作为索引称为Fancy Indexing
# [[0, 2, 4]]指定要访问的索引位置
# Fancy Indexing返回副本,不共享内存
fancy = original[[0, 2, 4]]  # Fancy Indexing创建副本
# 修改副本的第1个元素
fancy[0] = 777  # 修改副本的元素
# 原数组不受影响
print(f"Fancy索引修改副本,原数组: {original}")  # 输出:[1, 2, 3, 4, 5]

规则总结: - 切片 → 视图(共享内存) - Fancy索引 → 副本(独立内存) - 布尔索引 → 副本 - .copy() → 强制副本

最佳实践: 当不确定时,使用.copy()创建副本以避免意外修改。

5.6 数组的形状操作

列表 5.8
# ==================== 创建一维数组 ====================
# np.arange()创建0-9的一维数组
# 包含10个元素
arr_1d = np.arange(10)  # 创建一维数组
print(f"一维数组: {arr_1d}")  # 输出:[0 1 2 3 4 5 6 7 8 9]

# ==================== reshape改变形状 ====================
# .reshape()改变数组的形状而不改变数据
# (2, 5)表示转换为2行5列的二维数组
# 元素总数必须保持不变(10=2×5)
arr_2x5 = arr_1d.reshape(2, 5)  # 转换为2×5数组
print(f"\n2×5数组:\n{arr_2x5}")  # 输出二维数组

# ==================== reshape(-1)自动计算维度 ====================
# -1表示自动计算该维度的大小
# reshape(-1, 2)表示自动计算行数
# 10个元素,每行2列,自动计算为5行
arr_5x2 = arr_1d.reshape(-1, 2)  # 自动转换为5×2数组
print(f"\n5×2数组:\n{arr_5x2}")  # 输出5×2数组

# ==================== ravel和flatten(展平数组) ====================
# .ravel()将多维数组展平为一维
# 返回视图(可能),不保证创建副本
flat_ravel = arr_2x5.ravel()  # 展平为数组(可能是视图)

# .flatten()将多维数组展平为一维
# 返回副本,总是创建新数组
flat_flatten = arr_2x5.flatten()  # 展平为数组(总是副本)

# ==================== 输出展平结果 ====================
print(f"\nravel结果: {flat_ravel}")  # 输出展平后的数组
print(f"flatten结果: {flat_flatten}")  # 输出展平后的数组

# ==================== 转置 ====================
# .T属性返回数组的转置
# 行列互换:2×5变为5×2
transpose = arr_2x5.T  # 转置数组
print(f"\n转置后({transpose.shape}):\n{transpose}")  # 输出转置后的数组

5.7 广播(Broadcasting)机制

广播是NumPy的强大特性,允许不同形状的数组进行算术运算。

列表 5.9
# ==================== 场景1:标量与数组 ====================
# prices是包含5个价格的一维数组
prices = np.array([10, 20, 30, 40, 50])  # 价格数组

# discount是标量(单个数值)
discount = 0.9  # 打9折的折扣率

# 标量与数组运算时,标量自动"广播"到每个元素
# 相当于[10*0.9, 20*0.9, 30*0.9, 40*0.9, 50*0.9]
discounted_prices = prices * discount  # 广播乘法
print(f"折扣后价格: {discounted_prices}")  # 输出折扣后价格

# ==================== 场景2:不同形状的数组 ====================
# base_prices是3只股票的基准价格
base_prices = np.array([10, 20, 30])  # 3只股票价格(一维)

# adjustments是3×1的二维数组(列向量)
# 代表3种不同的调整情景
adjustments = np.array([[1.1], [0.9], [1.0]])  # 调整系数(列向量)

# 广播机制:
# base_prices形状(3,)广播为(3, 3)
# adjustments形状(3, 1)广播为(3, 3)
# 结果是3×3矩阵
result = base_prices * adjustments  # 广播计算
print(f"\n调整后的价格矩阵:\n{result}")  # 输出3×3矩阵

# ==================== 场景3:标准化收益率 ====================
# returns是2×3的收益率数组
# 2只股票,3个时间点
returns = np.array([[0.05, 0.03, 0.04], [0.06, 0.02, 0.05]])  # 收益率矩阵

# 沿axis=1计算每行的均值
# keepdims=True保持维度(结果为2×1而非(2,))
mean = returns.mean(axis=1, keepdims=True)  # 计算均值并保持维度

# 沿axis=1计算每行的标准差
# keepdims=True保持维度
std = returns.std(axis=1, keepdims=True)  # 计算标准差并保持维度

# 标准化:(收益率-均值)/标准差
# 广播机制使mean和std自动扩展到与returns相同的形状
normalized = (returns - mean) / std  # 标准化计算
print(f"\n标准化后的收益率:\n{normalized}")  # 输出标准化后的数据

广播规则: 1. 从尾部(最右边)开始比较维度 2. 维度相同或其中一个为1则兼容 3. 缺失的维度视为1

金融应用: - 批量调整: 对多只股票应用统一系数 - 风险调整: 按波动率调整收益率 - 组合优化: 权重向量与收益率矩阵相乘

5.8 性能优化向量化 vs 循环

列表 5.10
# ==================== 导入必要的库 ====================
# import time导入时间模块,用于性能测试
import time  # 导入时间模块

# ==================== 生成大数据集 ====================
# n定义数据规模
n = 1000000  # 100万个数据点

# np.random.randn()生成标准正态分布的随机数
# * 10 + 100将数据调整为均值100,标准差10
# 模拟股价数据
prices = np.random.randn(n) * 10 + 100  # 生成模拟股价数据

# ==================== 方法1:Python循环(慢) ====================
# 记录开始时间
start = time.time()  # 获取开始时间

# 使用for循环计算收益率
# []创建空列表,用于存储结果
returns_loop = []  # 初始化空列表
# range(1, len(prices))生成1到n-1的索引
for i in range(1, len(prices)):  # 遍历每个时间点
    # 计算简单收益率:(当前价格-前一日价格)/前一日价格
    ret = (prices[i] - prices[i-1]) / prices[i-1]  # 计算收益率
    returns_loop.append(ret)  # 添加到列表
# 计算循环耗时
loop_time = time.time() - start  # 计算耗时

# ==================== 方法2:NumPy向量化(快) ====================
# 记录开始时间
start = time.time()  # 获取开始时间

# NumPy向量化计算
# prices[1:]获取第2个到最后的元素
# prices[:-1]获取第1个到倒数第2个的元素
# 数组运算自动向量化,无需显式循环
returns_vec = (prices[1:] - prices[:-1]) / prices[:-1]  # 向量化计算收益率
# 计算向量化耗时
vec_time = time.time() - start  # 计算耗时

# ==================== 输出性能对比 ====================
# f-string格式化输出,:.4f保留4位小数
print(f"循环时间: {loop_time:.4f}秒")  # 输出循环耗时
print(f"向量化时间: {vec_time:.4f}秒")  # 输出向量化耗时
# 计算性能提升倍数
print(f"性能提升: {loop_time/vec_time:.1f}倍")  # 输出加速比